package com.txtsqlclient.domain;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class ReadQueryResult extends QueryResult {

	final protected List<List<String>> data;

	final protected int totalRows, numOfColumns;

	final protected List<String> header;

	protected boolean isDataSet, isHeaderSet;

	protected int pageSize, startRow, rowIndex = 0;

	protected ResultSet rs;

	private static Log LOGGER = LogFactory.getLog(ReadQueryResult.class);

	public ReadQueryResult(final ResultSet rs) throws QueryExecutionException {
		this.rs = rs;
		this.data = new ArrayList<List<String>>();
		this.header = new ArrayList<String>();
		try {
			final ResultSetMetaData metadata = this.rs.getMetaData();
			this.numOfColumns = metadata.getColumnCount();
			this.rs.last();
			this.totalRows = this.rs.getRow();
			// What if the ResultSet is FORWARD ONLY?
			this.rs.beforeFirst();
		} catch (SQLException se) {
			LOGGER.error("Error reading data from Result Set", se);
			throw new QueryExecutionException(
					"Error reading data from Result Set", se);
		}
		LOGGER.debug("Query result contains " + this.totalRows + " rows and "
				+ this.numOfColumns + " columns");
	}

	/**
	 * This method is required to get all rows for the query as a single list.
	 * 
	 * @return All rows of data for the Query.
	 * @throws QueryExecutionException
	 */
	public List<List<String>> getData() throws QueryExecutionException {
		if (!this.isDataSet) {
			this.data.addAll(getNextPage(1, this.getNumOfRowsReturned()));
			this.isDataSet = true;
		}
		return this.data;
	}

	public int getNumOfRowsReturned() throws QueryExecutionException {
		return this.totalRows;
	}

	public int getNumOfColumns() {
		return this.numOfColumns;
	}

	/**
	 * This method is required to get the header (list of column names) for the
	 * query.
	 * 
	 * @return The header for the Query.
	 * @throws QueryExecutionException
	 */
	public List<String> getHeader() throws QueryExecutionException {
		if (!this.isHeaderSet) {
			try {
				final ResultSetMetaData metadata = this.rs.getMetaData();
				for (int i = 0; i < this.numOfColumns; i++) {
					this.header.add(metadata.getColumnName(i + 1));
				}
			} catch (SQLException se) {
				LOGGER.error("Error while reading Result Set", se);
				throw new QueryExecutionException(
						"Error while reading Result Set", se);
			}
			this.isHeaderSet = true;
		}
		return this.header;
	}

	/**
	 * This method is required to get the header and all rows of data together
	 * for the query.
	 * 
	 * @return Header and data rows for the Query.
	 * @throws QueryExecutionException
	 */
	public List<List<String>> getHeadersAndData()
			throws QueryExecutionException {
		List<List<String>> result = new ArrayList<List<String>>();
		result.add(getHeader());
		result.addAll(getData());
		return result;
	}

	/**
	 * This method is required to get a 'page' of results as a single list.
	 * 
	 * @param startRow
	 *            The row number for the first record of the current page of
	 *            results.
	 * @param pageSize
	 *            The number of records per page.
	 * @return The list of records on the current page. If the result set has
	 *         fewer records than the page can hold (e.g. the last page), only
	 *         the remaining records are returned.
	 * @throws QueryExecutionException
	 */
	public List<List<String>> getNextPage(int startRow, int pageSize)
			throws QueryExecutionException {
		this.setStartRow(startRow);
		this.setPageSize(pageSize);
		List<List<String>> data = new ArrayList<List<String>>();
		List<String> row = null;
		while ((row = getNextRow()) != null) {
			data.add(row);
		}
		return data;
	}

	protected void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	/**
	 * This method is required to set the starting row. This can be used in
	 * conjunction with <code>setPageSize()</code> and
	 * <code>getNextRow()</code> for pagination.
	 * 
	 * @param startRow
	 *            The row number for the first record of the current page of
	 *            results.
	 * @throws QueryExecutionException
	 */
	protected void setStartRow(int startRow) throws QueryExecutionException {
		try {
			this.rs.absolute(startRow);
			this.startRow = startRow;
			this.rowIndex = startRow;
		} catch (SQLException se) {
			LOGGER.error("Error setting the start index for Result Set", se);
			throw new QueryExecutionException(
					"Error setting the start index for Result Set", se);
		}
	}

	/**
	 * This method is required to get the next row on the current page.
	 * 
	 * @return The next record as a list of 'cells' - if the next record exists
	 *         null - if there are no more records are present in the result set
	 *         or if the next record belongs to the next page.
	 * @throws QueryExecutionException
	 */
	protected List<String> getNextRow() throws QueryExecutionException {
		if (this.rowIndex++ < this.startRow + this.pageSize) {
			try {
				if (this.rs.next()) {
					List<String> data = new ArrayList<String>();
					for (int i = 0; i < this.numOfColumns; i++) {
						data.add(this.rs.getString(i + 1));
					}
					return data;
				} else {
					return null;
				}
			} catch (SQLException se) {
				LOGGER.error("Error reading next row from Result Set", se);
				throw new QueryExecutionException(
						"Error reading next row from Result Set", se);
			}
		}
		return null;
	}
}